<#
Force the object from command or function result to array.
Sometimes when a command returns zero or one object, it becomes $null or the object.
Only when a command returns multiple objects, it becomes an array.
This function let you get an array anyway to make writing script easier.
#>
function ForceArray($nodes) {
    if ($nodes -eq $null) {
        return ,@()
    } elseif (($nodes -is [System.Array]) -or ($nodes -is [System.Collections.ArrayList])) {
        return $nodes
    } else {
        return ,@($nodes)
    }
}

<#
Search all GacUI Xml Resource file names in the directory that contains GacUI.xml, which is specified by $FileName.
#>
function EnumerateResourceFiles([String] $FileName) {
    Write-Host "Searching for all resource files ..."

    <# Load GacUI.xml and find all path patterns that we want to exclude from our result #>
    [Xml]$gacui_xml = Get-Content $FileName
    $excludes = (ForceArray (Select-Xml -Xml $gacui_xml -XPath "//GacUI/Exclude/@Pattern")).Node.Value
    $search_directory = Split-Path -Parent (Resolve-Path $FileName)

    <# Enumerate all Xml file, if it is not excluded, and it matches <Resource><Folder name="GacGenConfig"/></Resource> #>
    $resource_files = (Get-ChildItem $search_directory -Filter "*.xml" -Recurse | ForEach-Object {
        $normalized_path = $_.FullName -replace '\\','/'
        if (($excludes | Where-Object { $normalized_path.Contains($_) }).Length -eq 0) {
            if ((Select-Xml -Path $_.FullName -XPath "//Resource/Folder[@name='GacGenConfig']") -ne $null) {
                $_.FullName.Substring($search_directory.Length)
            }
        }
    })
    [System.IO.File]::WriteAllLines("$($FileName).log\ResourceFiles.txt", $resource_files)
}

<#
Call GacGen32.exe to dump metadatas from resource files.
Input files is save in $($FileName).log\ResourceFiles.txt, which is generated by EnumerateResourceFiles function
Output files is specified in $ResourceDumpFiles(resource_file_name => dump_file_name)
#>
function DumpResourceFiles([String] $FileName, [HashTable]$ResourceDumpFiles) {
    Write-Host "Dumping all resource files ..."

    $search_directory = Split-Path -Parent (Resolve-Path $FileName)
    Get-Content "$($FileName).log\ResourceFiles.txt" | ForEach-Object {
        $input_file = Join-Path -Path $search_directory -ChildPath $_
        $output_file = "$($FileName).log\$($_ -replace '\\','_')"
        $ResourceDumpFiles[$input_file] = $output_file
    }

    $ResourceDumpFiles.Keys | ForEach-Object {
        $input_file = $_
        $output_file = $ResourceDumpFiles[$_]
        Start-Process-And-Wait (,("$PSScriptRoot\GacGen32.exe", "/D `"$($input_file)`" `"$($output_file)`"")) $true
        if (-not (Test-Path -Path $output_file)) {
            throw "Failed to dump GacUI Xml Resource File: " + $input_file
        }
    }
}

<#
Given a metadata dump, determine if the compile result of this resource file is outdated.
It collects last modify times of all input files and output files.
If any output files' time is eariler than any input files' time, it is outdated.
#>
function NeedBuild([Xml] $Dump) {
    $input_files = (ForceArray (Select-Xml -Xml $Dump -XPath "//ResourceMetadata/Inputs/Input/@Path")).Node.Value
    $output_files = (ForceArray (Select-Xml -Xml $Dump -XPath "//ResourceMetadata/Outputs/Output/@Path")).Node.Value

    if (($output_files | Where-Object { -not [System.IO.File]::Exists($_) }) -ne $null) {
        return $true
    }

    $input_file_times = ForceArray ($input_files | ForEach-Object {
        [System.IO.FileInfo]::new($_).LastWriteTimeUtc
    })
    $output_file_times = ForceArray ($output_files | ForEach-Object {
        [System.IO.FileInfo]::new($_).LastWriteTimeUtc
    })

    $outdated = $output_file_times | Where-Object {
        $output = $_
        $modifieds = $input_file_times | Where-Object {
            return $_ -gt $output
        }
        return $modifieds -ne $null
    }

    return $outdated -ne $null
}

<#
Given all metadata dumps $ResourceDumps(resource_file_name => Xml dump),
generate $name_to_file_map(resource_name => resource_file_name),
and $name_to_dep_map(resource_name => all dependencies)
#>
function ExtractDeps([HashTable] $ResourceDumps, [HashTable] $name_to_file_map, [HashTable] $name_to_dep_map)
{
    $ResourceDumps.Keys | ForEach-Object {
        $xml = $ResourceDumps[$_]
        $name = (Select-Xml -Xml $xml -XPath "//ResourceMetadata/ResourceMetadata/@Name").Node.Value
        if ($name -ne "") {
            $attrs = ForceArray (Select-Xml -Xml $xml -XPath "//ResourceMetadata/ResourceMetadata/Dependencies/Resource/@Name")
            $deps = ForceArray $attrs.Node.Value
            $name_to_file_map[$name] = $_
            $name_to_dep_map[$name] = [System.Collections.ArrayList]::new($deps)
        }
    }
}

<#
Given all metadata dumps $ResourceDumps(resource_file_name => Xml dump),
write all resources that is outdated to $OutputFileName.
#>
function EnumerateBuildCandidates([HashTable] $ResourceDumps, [String] $OutputFileName) {
    Write-Host "Finding resource files that need rebuild ..."
    $direct_candidates = ForceArray ($ResourceDumps.Keys | Where-Object { NeedBuild $ResourceDumps[$_] })

    $name_to_file_map = @{}
    $name_to_dep_map = @{}
    ExtractDeps $ResourceDumps $name_to_file_map $name_to_dep_map

    $file_to_name_map = @{}
    $name_to_file_map.Keys | ForEach-Object { $file_to_name_map[$name_to_file_map[$_]] = $_ }

    <# Get all names of named resources that are outdated #>
    $names = ForceArray ($direct_candidates | Where-Object { $file_to_name_map.ContainsKey($_) } | ForEach-Object { $file_to_name_map[$_] })
    $names = [System.Collections.ArrayList]::new($names)
    <# Get all names of named resources that are not outdated #>
    $pool = ForceArray ($name_to_file_map.Keys | Where-Object { -not $names.Contains($_) })
    $pool = [System.Collections.ArrayList]::new($pool)

    <# Grow $names from $pool #>
    while ($true)
    {
        <# If any resource in $pool depends on any resource in $name, it is moved from $pool to $names #>
        $selection = ForceArray ($pool | Where-Object {
            $deps = $name_to_dep_map[$_]
            return (ForceArray ($deps | Where-Object { $names.Contains($_) })).Count -ne 0
        })
        if ($selection.Count -eq 0) { break }

        $names.AddRange($selection)
        $selection | ForEach-Object { $pool.Remove($_) }
    }
    
    <# List all anonymous resource files before named resource files #>
    $anonymous_candidates = ForceArray ($direct_candidates | Where-Object { -not $name_to_file_map.ContainsValue($_) })
    $named_candidates = ForceArray ($names | ForEach-Object { $name_to_file_map[$_] })

    [System.IO.File]::WriteAllLines($OutputFileName, $anonymous_candidates + $named_candidates)
}

<#
Given all metadata dumps $ResourceDumps(resource_file_name => Xml dump),
Write all paths of anonymous resource files to $OutputFileName.
#>
function EnumerateAnonymousResources([HashTable] $ResourceDumps, [String] $OutputFileName) {
    Write-Host "Finding anonymouse resource files ..."
    $file_names = $ResourceDumps.Keys | Where-Object {
        return (ForceArray (Select-Xml -Xml $ResourceDumps[$_] -XPath "//ResourceMetadata/ResourceMetadata/@Name"))[0].Node.Value -eq ""
    } | Sort-Object
    [System.IO.File]::WriteAllLines($OutputFileName, (ForceArray $file_names))
}

<#
Verify if $name_to_dep_name(resource_name => dependencies) contains any dependencies that are not in this map
#>
function ValidateDeps([HashTable] $name_to_dep_map)
{
    $hasError = $false
    $name_to_dep_map.Keys | ForEach-Object {
        $key = $_
        $name_to_dep_map[$key] | ForEach-Object {
            if (-not $name_to_dep_map.ContainsKey($key)) {
                $hasError = $true
                Write-Host "Resource $($key) depends on $($_) but $($_) does not exist."
            }
        }
    }
    if ($hasError) { throw "Please check your metadata." }
}

<#
Sort all named resource files in partial order
#>
function SortDeps([HashTable] $name_to_dep_map)
{
    $compile_order = [System.Collections.ArrayList]::new()
    while ($name_to_dep_map.Count -gt 0) {
        $selection = ForceArray ($name_to_dep_map.Keys | Where-Object { $name_to_dep_map[$_].Count -eq 0 })
        if ($selection.Count -eq 0) {
            Write-Host "Found circle dependency in the following resources:"
            $name_to_dep_map.Keys | ForEach-Object { Write-Host "    $($_)" }
            $hasError = $true;
            break
        } else {
            $compile_order.AddRange((ForceArray ($selection | Select-Object)))
            $selection | ForEach-Object {
                $ready = $_
                $name_to_dep_map.Remove($ready)
                $name_to_dep_map.Values | ForEach-Object { $_.Remove($ready) }
            }
        }
    }
    if ($hasError) { throw "Please check your metadata." }
    return $compile_order
}

<#
Given all metadata dumps $ResourceDumps(resource_file_name => Xml dump),
write all paths of named resource files in the correct build order to $OutputNames,
with all "resource_name=>resource_file_path" to $OutputMapping.
The $OutputMapping will be consumed
    GacGen32.exe /P <resource-xml> <HERE>
    GacGen32.exe /P <resource-xml> <HERE>
    as an optional parameter
#>
function EnumerateNamedResources([HashTable] $ResourceDumps, [String] $OutputNames, [String] $OutputMapping) {
    Write-Host "Finding named resource files ..."

    $name_to_file_map = @{}
    $name_to_dep_map = @{}
    ExtractDeps $ResourceDumps $name_to_file_map $name_to_dep_map
    ValidateDeps $name_to_dep_map
    $compile_order = ForceArray (SortDeps $name_to_dep_map)

    $file_names = ForceArray ($compile_order | ForEach-Object {
        return $name_to_file_map[$_]
    })
    $file_mapping = ForceArray ($name_to_file_map.Keys | ForEach-Object {
        return "$($_)=>$($name_to_file_map[$_])"
    } | Sort-Object)
    [System.IO.File]::WriteAllLines($OutputNames, $file_names)
    [System.IO.File]::WriteAllLines($OutputMapping, $file_mapping)
}